<?php
// FileFind - ftp/shares indexing and search php engine,
// http://project.ffind.ru
//
// Module: class.host.crawl.php
// Description: Class for crawling host
// Copyright (c) 2003-2008 Rage Steel, www.gt2.ru
// ----------------------------------------------
// $Id$

// REFACTORING Имена файлов листинга сделать с путём и положить их в переменные
// Только определиться, кто будет их туда помещать, наверное надо load() переопределять.

// TODO Сделать единую функцию анализа пригодности листинга для индексации.
// Нужно обязательно проверять, чтобы он был более молодой, чем последняя индексация в базе данных.

// TODO Дополнительное поле listing_location tinytext NULL, если оно существует — то брать
// листниг из того места, вместо стандартной процедуры

/**
 * Расширение класса узла для того чтобы не включать код связанный с листингом в базовый класс узла.
 */
class CHostCrawl extends CHost {
  /**
   * Была-ли индексация, если true то при разблокировке обновить дату последней индексации
   *
   * @var boolean
   */
  var $is_crawl = false; 
  var $is_ftp_resume = false; // продолжение обхода ftp-узла.
  
  /**
   * Обёртка для работы с листингом
   *
   * @var CHostBaseListing
   */
  var $listing;
  
  // Корневой каталог
  var $root_dir;
  
  /**
   * Имя файла листинга
   * 
   * @var string
   */
  var $listing_file;
  
  /**
   * Имя временного файла листинга
   *
   * @var string
   */
  var $listing_file_tmp;

  // Возвращает идентификатор корневого каталога.
  // Если он не существует - то корневой каталог принудительно создаётся
  function get_root_dir_id() {
    if (!is_null($this->root_dir_id)) {
      return $this->root_dir_id;
    }

    $this->create_root_dir();
    $this->update_field('root_dir_id', $this->root_dir_id);
    return $this->root_dir_id;
  } // get_root_dir_id

  function load($host_name = '', $ip_search = false) {
    $result = parent::load($host_name, $ip_search);

    $this->listing_file = '../listing/' . $this->host_type . '-' . $this->host_name;
    $this->listing_file_tmp = $this->listing_file . '.tmp';

    return $result;
  } // load

  /**
   *  Попытка блокировки узла перед проверкой состояния или индексацией
   *
   * @param integer $set_status Причина блокировки, побитово добавляется к статусу узла
   * @return boolean
   */
  function lock($set_status = 0) {
    
    $sq = $this->host_tbl();
    $sq->where .= ' AND `lock_pid` IS NULL';

    $sq->add_value('lock_pid', getmypid());
    $sq->add_calc_field('lock_datetime', 'NOW()');

    if ($set_status != 0) {
      $sq->add_calc_field('host_status', "`host_status` | $set_status");
    }

    if (!$sq->exec_update()) {
      return false;
    }
    
    return (1 == $sq->affected_rows());
  } // lock

  /**
   * Снятие блокировки с узла
   * 
   * @param integer $reset_status Причина блокировки, побитово вычитается из статуса узла
   */
  function unlock($reset_status = 0) {
    $sq = $this->host_tbl();

    $sq->add_value('lock_pid', null);
    $sq->add_value('lock_datetime', null);

    if ($reset_status != 0) {
      $sq->add_calc_field('host_status', "`host_status` & ~$reset_status");
    }

    if ($this->is_crawl) {
      $sq->add_value('crawl_last', $this->conn_datetime);
    }

    $sq->exec_update();
  } // unlock

  /**
   * Получение списка файлов узла
   * если есть сохранённый файл, не старше $max_listing_age секунд то листинг читается оттуда
   *
   * @param integer $max_listing_age Максимальный возраст листинга в секундах
   * @return integer Результат ->connect или -1, если листинг был считан из файла
   */
  function get_listing($max_listing_age) {
    if (128 == ($this->host_opt & 128)) {
      require_once('../crawl/lib.fsa.php');
      return get_fsa_rawlist($this);
    }
    
    if ($this->load_rawlist($this->listing_file, $max_listing_age)) {
      return -1;
    } else {
    	if (!$this->in_current_group) {
    		// 
    		return 6;
    	}
    }

    $ping = $this->connect(2);
    if (0 == $ping) {
      if (!$this->save_rawlist($this->listing_file)) {
        echo "Error saving listing!\n";
      }
    }

    return $ping;
  } // get_listing

  /**
   * Загрузка списка файлов из сохраненного файла, если указан возраст, то не старше заданного возраста
   * Если узел не принадлежит группе существующий файл загружается в том случае, даже если его возраст больше, 
   * но при этом он имеет более позднюю дату создания чем последняя индексация
   *
   * @param string $listing_file Имя файла
   * @param integer $max_listing_age Максимальный возраст сохранённого файла в секунда, если существующий файл старше — он не считывается
   * @return boolean
   */
  function load_rawlist($listing_file, $max_listing_age = null) {
    if (!file_exists($listing_file)) {
    	return false;
    }
    
    $mtime = filemtime($listing_file);
    if (!is_null($max_listing_age)) {
    	if (time() > $mtime + $max_listing_age) {
            echo ', listing found, but outdated';
	    	return false;
	    } elseif (!$this->in_current_group) {
	    	if ($mtime > strtotime($this->crawl_last)) {
                echo ', listing exists, but outdated';
	    		return false;
	    	}
	    }
    }

    $this->conn_datetime = get_sql_datetime($mtime);
    $this->rawlist = file($listing_file);
    $this->rawlist_count = count($this->rawlist);

    return true;
  } // load_rawlist

  function save_rawlist($listing_file) {
    if ($f = fopen($listing_file, 'w')) {
      reset($this->rawlist);
      while(list(, $line) = each($this->rawlist)) {
        fwrite($f, "$line\n");
      }
      fclose($f);
      touch($listing_file, strtotime($this->conn_datetime));
      return true;
    }

    return false;
  } // save_rawlist

  function kill_rawlist($listing_file) {
    if (file_exists($listing_file)) {
      return unlink($listing_file);
    } else {
      return true;
    }
  } // kill_rawlist

  // Подготовиться к синхронизации - при необходимости
  // прочитать создать объект листинга и каталога
  function prepare_sync() {
    $has_listing = is_object($this->listing);
    $has_root_dir = is_object($this->root_dir);

    if ($has_listing && $has_root_dir) {
      return true;
    }

    if (HOST_TYPE_FTP == $this->host_type) {
      if (!$has_listing) {
        $this->listing = new CHostFtpListing($this->rawlist);
      }
      if (!$has_root_dir) {
        $this->root_dir = new CHostFtpDir($this->listing, '/');
      }
    } elseif (HOST_TYPE_SHARE == $this->host_type) {
      if (!$has_listing) {
        $this->listing = new CHostShareListing($this->rawlist);
      }
      if (!$has_root_dir) {
        $this->root_dir = $this->listing->get_dir('/');
      }
    } else {
      echo "  Unknown host_type, skipping\n";
      return false;
    }

    return true;
  } // prepare_sync

  // Синхронизация полученного содержимого узла с базой
  function sync() {
    global $appearance_date, $sync_stat;

    $start = RS_time_passed();

    // Остается только проиндексировать
    if (!$this->prepare_sync()) {
      return false;
    }
    $this->listing->init_progress();

    $root_dirtbl = new CDirTbl($this->get_root_dir_id());

    // Для новых хостов дата появления их файлов всегда равна нулевой
    $appearance_date = $this->is_new() ? '0000-00-00' : $this->conn_datetime;
    
    reset_sync_stat();

    dir_sync(
      $this->root_dir, 
      $root_dirtbl
    );

    $dur = (RS_time_passed() - $start) * 10;
    $dur = (int)$dur;
    $sync_stat['dur'] = $dur;
    $this->log_crawl($dur);

    $this->is_crawl = true;

    return true;
  } // sync

  function change_status($status) {
    $sq = $this->host_tbl();
    $sq->add_calc_field('host_status', '`host_status` ' . $status);
    $sq->exec_update();
    unset($sq);
  } // change_status

  // Записать информацию в журнал индексации,
  // $dur - длительность индексации в секундах * 10
  function log_crawl($dur) {
    global $sync_stat;

    $sq = new CSqlQ('tbl_crawl');
    $sq->add_value('host_id', $this->host_id);
    $sq->add_calc_field('crawl_datetime', 'NOW()');
    $sq->add_value('crawl_dur', $dur);
    $sq->add_value('crawl_count', $this->rawlist_count);

    $sq->add_value('stat_sync_count',    $sync_stat['sync_count']);
    $sq->add_value('stat_dirs_added',    $sync_stat['dirs_added']);
    $sq->add_value('stat_dirs_removed',  $sync_stat['dirs_removed']);
    $sq->add_value('stat_files_added',   $sync_stat['files_added']);
    $sq->add_value('stat_files_removed', $sync_stat['files_removed']);
    $sq->add_value('stat_files_changed', $sync_stat['files_changed']);

    $sq->exec_insert();
    unset($sq);
  } // log_crawl

  // Проверка полноты листинга
  // Возвращает номер ошибки или false - если листинг неполон
  function is_listing_full() {
    $dir_count = &$this->listing->dir_count;

    if (0 == $dir_count) {
      // Если каталогов нет вообще - это определенно ошибка
      return 110;
    }

    if ($dir_count > 1) {
      // Если больше одного каталога - считаем что ls -lR есть
      return 0;
    }

    // Дальше всё выполняется только в том случае, если каталог один - корневой

    $this->root_dir = new CHostFtpDir($this->listing, '/');
      
    if (!$this->root_dir->load()) {
      // Ошибка загрузки каталога
      return 111;
    }

    if (!$this->root_dir->read_dir()) {
      // Ошибка чтения каталога
      return 112;
    }

    if (!$this->root_dir->has_sub_dirs()) {
      // Нет подкаталогов - значит все нормально, листинг полон
      return 0;
    }

    return false;
  } // is_listing_full

  // Проверка, нужно ли делать полный обход ftp-узла
  function check_ftp_listing($ftp) {
    if (!$this->is_ftp_resume) {
      if (0 == count($this->rawlist)) {
        return -1;
      }

      $this->listing = new CHostFtpListing($this->rawlist);
      $result = $this->is_listing_full();
      if (is_int($result)) {
        return $result;
      }
    }

    // Попали сюда, значит нам нужно полностью обходить ftp'шник.
    if ($this->get_ftp_dir($this->root_dir, $ftp)) {
      // Похоже что всё хорошо
      $this->kill_rawlist($this->listing_file_tmp);
      $this->update_field('resume_path', NULL);
      $this->rawlist_count = count($this->rawlist);
      return 0;
    } else {
      // Тут обрыв. Надо сохранить наработанное
      $this->update_field('resume_path', $this->resume_path);
      $this->save_rawlist($this->listing_file_tmp);
      return 113;
    }
  } // check_ftp_listing

  function resume_ftp_listing($ftp) {
    if (!$this->load_rawlist($this->listing_file_tmp)) {
      // Не прочитали старое - надо начинать всё сначала
      return -1;
    };

    $this->is_ftp_resume = true;

    $this->listing = new CHostFtpListing($this->rawlist);
    
    // Без проверки на ошибки, т.к. считаем там уже и так всё нормально
    // ибо в первый раз считалось без ошибок
    $this->root_dir = new CHostFtpDir($this->listing, '/');
    $this->root_dir->load();
    $this->root_dir->read_dir();

    return $this->check_ftp_listing($ftp);
  } // resume_ftp_listing

  // Получить содержимое подкаталогов заданнаго каталога
  function get_ftp_dir(&$dir, $ftp) {

    if ($this->is_ftp_resume) {
      // Восстановление - переход к каталогу, на котором остановились
      if ($dir->path == $this->resume_path) {
        // Этот каталог есть точка останова, 
        // дальше переходим к получению содержимого с ftp
        $this->is_ftp_resume = false;
      }
    } else {
      // Режим получения листингов
      // Значит если мы здесь, то этот ($dir) каталог успешно прочитан
      // И с него нужно будет продолжать
      $this->resume_path = $dir->path;
    }

    $files = &$dir->files;
    reset($files);
    while(list($fname, $data) = each($files)) {
      if ($data['isdir']) {
        $path = $dir->path . $fname . '/';

        if ($this->is_ftp_resume) {
          if ($path == substr($this->resume_path, 0, strlen($path))) {
            // Это родитель (или сам последний каталог)
            // Идём в него
            echo "=";
            $r_dir = $dir->sub_dir($fname);
            $r_dir->load();
            $r_dir->read_dir();
            $this->get_ftp_dir($r_dir, $ftp);
          } else {
            // Пропускаем "соседа"
            echo "-";
          }
          continue;
        }

        $sub_raw = ftp_rawlist($ftp, $path);

        // Таким образом листинг каталога принимается только в том случае, 
        // если после этого ftp_systype что-нибудь вернёт.
        // Я решил использовать именно systype ибо как я себе 
        // понимаю его должны поддерживать все сервера.

        $is_alive = ftp_systype($ftp) !== false;

        if ($is_alive) {
          if (is_array($sub_raw)) {
            echo ".";
          } else {
            echo "!";
            continue;
          }

          $dir_begin = count($this->rawlist) + 2;
          $dir_end = $dir_begin + count($sub_raw) - 1;

          // Нужно добавить это в rawlist
          $this->rawlist = array_merge($this->rawlist, array('', $path . ':'), $sub_raw);

          // Добавить dir_ranges
          $this->listing->dir_ranges[$path] = array('begin' => $dir_begin, 'end' => $dir_end);

          // Создадим объект и загрузить/прочитать
          $sub_dir = $dir->sub_dir($fname);

          if (!$sub_dir->load()) {
            continue;
          }

          if (!$sub_dir->read_dir()) {
            continue;
          }

          if (!$this->get_ftp_dir($sub_dir, $ftp)) {
            return false;
          }

        } else {
          // Нафиг выходим, обрыв
          // TODO А тут можно попытаться сделать переконнект после некоторой паузы
          // Только ещё надо зафиксировать предыдущий каталог, чтобы потом продолжить
          return false;
        }
        sleep(1);
      }
    }
    return true;
  } // get_ftp_dir

} // CHostCrawl

// Класс который будет оперировать не с массивом $rawlist в памяти
// А с handle потока

// Итак, сначала мы научимся работать с существующим листингом
// Пока только для FTP
class CHostCrawlStream extends CHostCrawl {

  // handle файла с листингом
  var $listing_handle = NULL;

//  var $ftp_client;

  function connect_ftp($action) {
    global $cfg;

    $ftp = new CFtpClient();

    if (!$ftp->connect($this->host_ip)) {
      return 3;
    };

//    if (2 == $action) {
//      ftp_set_option($ftp, FTP_TIMEOUT_SEC, 20);
//    }

    $result = 0;

    if ($ftp->login('anonymous', $cfg['ftp_login'])) {
      if ($pwd = $ftp->get_current_dir()) {
        if ($systype = $ftp->get_systype()) {
          if ((0 == $action) || (1 == $action)) {
            if (is_array($this->rawlist = $ftp->get_listing('/'))) {
              // TODO попытка разбора содержимого каталога
            } else {
              $result = 107;
            }
          } elseif ($action == 2) {
            //ftp_set_option($ftp, FTP_TIMEOUT_SEC, $cfg['ftp_lslr_timeout']);
            
            // Получать листинг или нет
            $get_root = true;
            
            if (!is_null($this->resume_path)) {
              // Продолжаем получение листинга
              $get_root = false;
              $result = $this->resume_ftp_listing($ftp);
              if (-1 == $result) {
                $get_root = true;
                echo "#";
              }
            }
            
            if ($get_root) {
              $this->kill_rawlist($this->listing_file_tmp);
              $this->listing_handle = fopen($this->listing_file_tmp, 'w+');
              
              if ($ftp->get_listing('/', true, $this->listing_handle)) {
                // Успешно получили листинг
                
                // Вызов функции проверки на полноты листинга
                $result = $this->check_ftp_listing($ftp);
                
                if (-1 == $result) {
                  // Значит пока мы не можем обработать листинг
                  $this->update_field('host_opt', $this->host_opt | 64);
                  $result = 0;
                } else {
                  $this->update_field('host_opt', $this->host_opt & (~64));
                }
              } else {
//print_r($ftp);
                $result = 108;
              }
            }
          }
        } else {
          $result = 106;
        }
      } else {
        $result = 105;
      }
    } else {
      $result = 104;
    }
    $ftp->disconnect();
    return $result;
  } // connect_ftp

  // Проверка, нужно ли делать полный обход ftp-узла
  // И запуск этого прохода
  function check_ftp_listing(&$ftp) {
    
    if (!$this->is_ftp_resume) {
      if (0 == ftell($this->listing_handle)) {
        return -1;
      }
      
      $this->listing = new CHostFtpListingStream($this->listing_handle);
      if ('' != $this->listing->error) {
        echo 'Error creating listing: ', $this->listing->error, "\n";
        return -1;
      }
      $result = $this->is_listing_full();
      if (is_int($result)) {
        $this->rename_listing();
        return $result;
      }
    }
    
    // Попали сюда, значит нам нужно полностью обходить ftp'шник.
    if ($this->get_ftp_dir($this->root_dir, $ftp)) {
      // Похоже что всё хорошо
      $this->update_field('resume_path', NULL);
      $this->rename_listing();
      return 0;
    } else {
      // Тут обрыв. Надо сохранить наработанное
      $this->update_field('resume_path', $this->resume_path);
      fclose($this->listing_handle);
      return 113;
    }
  } // check_ftp_listing

  // Переименовать файл листинга
  // И переоткрыть его
  function rename_listing() {
      fclose($this->listing_handle);
      $this->kill_rawlist($this->listing_file);
      rename($this->listing_file_tmp, $this->listing_file);
      $this->listing_handle = fopen($this->listing_file, 'r');
      $this->listing->handle = $this->listing_handle;
  } // rename_listing

  // Получить содержимое каталога, $dir
  function get_ftp_dir(&$dir, &$ftp) {

    if ($this->is_ftp_resume) {
      // Восстановление - переход к каталогу, на котором остановились
      if ($dir->path == $this->resume_path) {
        // Этот каталог есть точка останова, 
        // дальше переходим к получению содержимого с ftp
        $this->is_ftp_resume = false;
      }
    } else {
      // Режим получения листингов
      // Значит если мы здесь, то этот ($dir) каталог успешно прочитан
      // И с него нужно будет продолжать
      $this->resume_path = $dir->path;
    }
    
    $files = &$dir->files;
    reset($files);
    while(list($fname, $data) = each($files)) {
      if ($data['isdir']) {
        $path = $dir->path . $fname . '/';
        
        if ($this->is_ftp_resume) {
          if ($path == substr($this->resume_path, 0, strlen($path))) {
            // Это родитель (или сам последний каталог)
            // Идём в него
            echo '=';
            $r_dir = $dir->sub_dir($fname);
            $r_dir->load();
            $r_dir->read_dir();
            $this->get_ftp_dir($r_dir, $ftp);
          } else {
            // Пропускаем "соседа"
            echo '-';
          }
          continue;
        }
        
        $dir_pos0 = ftell($this->listing_handle);
        // Добавить сразу заголовок каталога
        fwrite($this->listing_handle, "$path:\n");
        $dir_pos = ftell($this->listing_handle);
        
        $listing_result = $ftp->get_listing($path, false, $this->listing_handle);
        
        // Таким образом листинг каталога принимается только в том случае, 
        // если после этого ftp_systype что-нибудь вернёт.
        // Я решил использовать именно systype ибо как я себе 
        // понимаю его должны поддерживать все сервера.
        
        $is_alive = $ftp->get_systype() !== false;
        
        if ($is_alive) {
          if (false !== $listing_result) {
            echo '.';
          } else {
            echo '!';
            continue;
          }
          
          // Добавить dir_ranges
          $this->listing->dir_ranges[$path] = array('pos' => $dir_pos, 'count' => $listing_result);
          $this->listing->rawlist_count += $listing_result + 1;
          
          // Создадим объект и загрузить/прочитать
          $last_post = ftell($this->listing_handle);
          $sub_dir = $dir->sub_dir($fname);
          fseek($this->listing_handle, $last_post);
          
          if (!$sub_dir->load()) {
            continue;
          }
          
          if (!$sub_dir->read_dir()) {
            continue;
          }
          
          if (!$this->get_ftp_dir($sub_dir, $ftp)) {
            return false;
          }
          
        } else {
          // Уберём записанный заголовок
          ftruncate($this->listing_handle, $dir_pos0);
          // Нафиг выходим, обрыв
          // TODO А тут можно попытаться сделать переконнект после некоторой паузы
          // Только ещё надо зафиксировать предыдущий каталог, чтобы потом продолжить
          return false;
        }
        sleep(1);
      }
    }
    return true;
  } // get_ftp_dir

  function resume_ftp_listing(&$ftp) {
    // Сначала проверка - есть-ли файл
    if (!file_exists($this->listing_file_tmp)) {
      return -1;
    }
    $this->listing_handle = fopen($this->listing_file_tmp, 'w+');
    if (!is_resource($this->listing_handle)) {
      return -1;
    };
    
    $this->is_ftp_resume = true;
    
    $this->listing = new CHostFtpListingStream($this->listing_handle);
    
    // Без проверки на ошибки, т.к. считаем там уже и так всё нормально
    // ибо в первый раз считалось без ошибок
    $this->root_dir = new CHostFtpDir($this->listing, '/');
    $this->root_dir->load();
    $this->root_dir->read_dir();
    
    return $this->check_ftp_listing($ftp);
  } // resume_ftp_listing

  function load_rawlist($listing_file, $max_listing_age = null) {
    if (HOST_TYPE_FTP != $this->host_type) {
      return parent::load_rawlist($listing_file, $max_listing_age);
    }
    
    if (file_exists($listing_file)) {
      
      $this->conn_datetime = get_sql_datetime(filemtime($listing_file));
      $this->listing_handle = fopen($listing_file, 'r');
      return is_resource($this->listing_handle);
    }
    
    return false;
  } // load_rawlist

  function save_rawlist($listing_file) {
    if (HOST_TYPE_FTP != $this->host_type) {
      return parent::save_rawlist($listing_file);
    }
    
    // Тут нам ничего делать не нужно, т.к. всё сохранение будет сделано во время подключения
    return true;
  } // save_rawlist

  function prepare_sync() {
    if (HOST_TYPE_FTP == $this->host_type) {
      if (!is_object($this->listing)) {
        $this->listing = new CHostFtpListingStream($this->listing_handle);
      }
      $this->rawlist_count = $this->listing->rawlist_count;
    }
    return parent::prepare_sync();
  } // prepare_sync

  function sync() {
    $result = parent::sync();
    
    if (is_resource($this->listing_handle)) {
      fclose($this->listing_handle);
    }
    
    return $result;
  } // sync
} // CHostCrawlStream

?>